Skip to main content

Async Runtimes

Why do we need an async runtime

Rust gives you: async fn, Future, .await

But Rust does not give you: A scheduler, An event loop, IO drivers, Timers

This is intentional.

Rust async is runtime-agnostic.

So this code:

async fn hello() {
println!("hello");
}

fn main() {
hello(); // creates a future, does not run it
}

Needs something to:

  • Poll the future
  • Wake it when it can make progress
  • Manage IO and timers

That “something” is an async runtime.

What is an async runtime?

An async runtime is a library that provides:

An executor + IO reactor + timer + task scheduler

All working together.

Conceptually:

          ┌─────────────┐
│ Executor │
└──────┬──────┘
│ polls
┌──────▼──────┐
│ Futures │
└──────┬──────┘
│ register wakers
┌──────▼──────┐
│ Reactor │ ← epoll / kqueue / IOCP
└──────┬──────┘
│ wake
┌──────▼──────┐
│ Scheduler │
└─────────────┘

Core components of an async runtime

Executor

  • Polls futures
  • Stops polling when they return Pending
  • Resumes them when woken

This is the heart of async.

Scheduler

Decides:

  • Which task runs next
  • On which thread
  • When to yield

Schedulers can be:

  • Single-threaded
  • Multi-threaded (work stealing)

Reactor (IO driver)

Listens for:

  • Socket readiness
  • File IO completion
  • OS events

Uses OS primitives:

  • Linux: epoll
  • macOS: kqueue
  • Windows: IOCP

Timer system

Handles: sleep, timeout, interval-based tasks

Tokio runtime (industry standard)

Tokio is:

  • High performance
  • Highly configurable
  • Production-grade
  • Used by: AWS, Discord, Cloudflare, etc.

Tokio architecture

  • Multi-threaded work-stealing scheduler
  • Separate IO reactor
  • Very explicit APIs

Example: Basic Tokio runtime

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
println!("start");

sleep(Duration::from_secs(1)).await;

println!("end");
}

What #[tokio::main] does

It expands to roughly:

fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
// your async main
});
}

Tokio task spawning and scheduling

use tokio::time::{sleep, Duration};

async fn worker(id: u32) {
println!("worker {} started", id);
sleep(Duration::from_secs(1)).await;
println!("worker {} finished", id);
}

#[tokio::main]
async fn main() {
for i in 1..=3 {
tokio::spawn(worker(i));
}

sleep(Duration::from_secs(2)).await;
}
  • tokio::spawn creates a task
  • Tasks are scheduled across runtime threads
  • .await yields control cooperatively

Blocking vs non-blocking in Tokio

Blocking (bad)

std::thread::sleep(Duration::from_secs(1));

This blocks a runtime worker thread.

Correct way

tokio::time::sleep(Duration::from_secs(1)).await;

CPU-bound work

tokio::task::spawn_blocking(|| {
heavy_computation();
});

This moves blocking work to a dedicated thread pool.

async-std runtime (stdlib-like design)

async-std aims to feel like Rust’s standard library.

Philosophy: Simple, Familiar APIs, Less configuration, Opinionated defaults

async-std example

use async_std::task;
use std::time::Duration;

async fn work() {
println!("working...");
task::sleep(Duration::from_secs(1)).await;
println!("done");
}

fn main() {
task::block_on(work());
}

Key differences

  • No macros required
  • block_on explicitly starts runtime
  • API mirrors std::thread

Spawning tasks in async-std

use async_std::task;

async fn worker(id: u32) {
println!("worker {} started", id);
task::sleep(std::time::Duration::from_secs(1)).await;
println!("worker {} finished", id);
}

fn main() {
task::block_on(async {
for i in 1..=3 {
task::spawn(worker(i));
}
});
}

Tokio vs async-std (practical comparison)

FeatureTokioasync-std
PerformanceVery highGood
EcosystemHugeSmaller
ConfigurationVery flexibleMinimal
API styleExplicitstd-like
Industry useDominantModerate
Learning curveSteeperEasier

Mixing runtimes (don’t)

  • Tokio futures expect Tokio reactor
  • async-std futures expect async-std reactor
  • Mixing causes deadlocks or panics

Use one runtime per application.

When to choose which runtime

Choose Tokio if:

  • You’re building servers
  • You need max performance
  • You depend on popular crates (hyper, tonic, axum)

Choose async-std if:

  • You want simplicity
  • You’re building small tools
  • You like std-like APIs

Mental model summary

An async runtime is:

A loop that keeps polling futures, listens to the OS for events, and wakes tasks when progress is possible.

Or simpler:

“The engine that makes .await actually work.”